package com.googlecode.horncomb.orm.ibatis.plugins;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;

import org.apache.ibatis.builder.xml.dynamic.ForEachSqlNode;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.ExecutorException;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.property.PropertyTokenizer;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeException;
import org.apache.ibatis.type.TypeHandler;
import org.apache.ibatis.type.TypeHandlerRegistry;

/**
 * 为了修改BaseTypeHandler中setNull时的jdbcType.
 * @author RUN
 */
@Intercepts({ @Signature(type = ParameterHandler.class, method = "setParameters", args = { PreparedStatement.class }) })
public class ParameterHandlerPlugin implements Interceptor {

	public Object intercept(Invocation invocation) throws Throwable {
		ParameterHandler paramHandle = (ParameterHandler) invocation
				.getTarget();
		MappedStatement mappedStatement = (MappedStatement) ReflectUtil
				.getField(paramHandle, "mappedStatement");
		BoundSql boundSql = (BoundSql) ReflectUtil.getField(paramHandle,
				"boundSql");
		Object parameterObject = paramHandle.getParameterObject();

		this.init(mappedStatement, parameterObject, boundSql);
		this.setParameters((PreparedStatement) invocation.getArgs()[0]);

		return null;
	}

	public Object plugin(Object target) {
		return Plugin.wrap(target, this);
	}

	public void setProperties(Properties properties) {

	}

	private TypeHandlerRegistry typeHandlerRegistry;
	private MappedStatement mappedStatement;
	private Object parameterObject;
	private BoundSql boundSql;
	private Configuration configuration;

	private void init(MappedStatement mappedStatement, Object parameterObject,
			BoundSql boundSql) {
		this.mappedStatement = mappedStatement;
		this.configuration = mappedStatement.getConfiguration();
		this.typeHandlerRegistry = mappedStatement.getConfiguration()
				.getTypeHandlerRegistry();
		this.parameterObject = parameterObject;
		this.boundSql = boundSql;
	}

	private void setParameters(PreparedStatement ps) throws SQLException {
		ErrorContext.instance().activity("setting parameters")
				.object(mappedStatement.getParameterMap().getId());
		List<ParameterMapping> parameterMappings = boundSql
				.getParameterMappings();
		if (parameterMappings != null) {
			MetaObject metaObject = parameterObject == null ? null
					: configuration.newMetaObject(parameterObject);
			for (int i = 0; i < parameterMappings.size(); i++) {
				ParameterMapping parameterMapping = parameterMappings.get(i);
				if (parameterMapping.getMode() != ParameterMode.OUT) {
					Object value;
					String propertyName = parameterMapping.getProperty();
					PropertyTokenizer prop = new PropertyTokenizer(propertyName);
					if (parameterObject == null) {
						value = null;
					} else if (typeHandlerRegistry
							.hasTypeHandler(parameterObject.getClass())) {
						value = parameterObject;
					} else if (boundSql.hasAdditionalParameter(propertyName)) {
						value = boundSql.getAdditionalParameter(propertyName);
					} else if (propertyName
							.startsWith(ForEachSqlNode.ITEM_PREFIX)
							&& boundSql.hasAdditionalParameter(prop.getName())) {
						value = boundSql.getAdditionalParameter(prop.getName());
						if (value != null) {
							value = configuration.newMetaObject(value)
									.getValue(
											propertyName.substring(prop
													.getName().length()));
						}
					} else {
						value = metaObject == null ? null : metaObject
								.getValue(propertyName);
					}
					TypeHandler typeHandler = parameterMapping.getTypeHandler();
					if (typeHandler == null) {
						throw new ExecutorException(
								"There was no TypeHandler found for parameter "
										+ propertyName + " of statement "
										+ mappedStatement.getId());
					}
					// hack the setParameter method
					if (typeHandler instanceof BaseTypeHandler) {
						BaseTypeHandler bth = (BaseTypeHandler) typeHandler;
						setParameter(bth, ps, i + 1, value,
								parameterMapping.getJdbcType());
					} else {
						typeHandler.setParameter(ps, i + 1, value,
								parameterMapping.getJdbcType());
					}

				}
			}
		}
	}

	private void setParameter(BaseTypeHandler typeHandler,
			PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
			throws SQLException {
		if (parameter == null) {
			if (jdbcType == null) {
				try {
					// the original is JdbcType.OTHER.TYPE_CODE
					ps.setNull(i, JdbcType.NULL.TYPE_CODE);
				} catch (SQLException e) {
					throw new TypeException(
							"Error setting null parameter.  Most JDBC drivers require that the JdbcType must be specified for all nullable parameters. Cause: "
									+ e, e);
				}
			} else {
				ps.setNull(i, jdbcType.TYPE_CODE);
			}
		} else {
			typeHandler.setNonNullParameter(ps, i, parameter, jdbcType);
		}
	}
}
